package ru.hh.team02.chat.client;

import ru.hh.team02.chat.messaging.Message;
import ru.hh.team02.chat.messaging.SendingQueue;
import ru.hh.team02.chat.server.Services;
import ru.hh.team02.chat.utils.StringSocket;

import java.io.IOException;
import java.net.Socket;

import java.util.List;
// XXX [Oleg] Please, add docs to the methods when you create ones.
// Without them the code is really really difficult to read and comprehend.

/**
 * class fully responsible for communicating with client computer
 */
public class Client implements Runnable {

    private StringSocket socket;
    private String name = "";
    private final SendingQueue sendingQueue = new SendingQueue(1, this);
    private MessageHandler messageHandler = null;

    /**
     * Create empty client for testing purposes.
     */
    public Client(String name) {
        this.name = name;
    }

    public Client(Socket socket) throws IOException {
        this.socket = new StringSocket(socket);
    }

    public String getName() {
        return name;
    }


    @Override
    public void run() {
        try {
            socket.write("Connected");
            Services.ADMIN_LOGGER.write("New client has connected");

            // Register client in the registry, with his sendingQueue sending switched off.
            // From now on the new messages will be added to the queue, but won't be sent.
            boolean result = registerClient();
            if (!result) return;

            writeWelcomeMessage();

            // Bind a message handler to the client
            messageHandler = new MessageHandler(getName());

            // Add all undelivered messages to the beginning of sending queue from the DB.
            Services.MESSENGER.sendUndeliveredMessages(getName());

            // Start sender threads
            sendingQueue.start();

            String message;
            do {
                message = socket.read();
                handleMessage(message);
            } while (!message.equals("exit"));
            Services.ADMIN_LOGGER.write("Client \"" + this.name
                    + "\" has disconnected");
        } catch (IOException e) {
            Services.ADMIN_LOGGER.write("Client went down");
        } catch (Exception e) {
            Services.ADMIN_LOGGER.write("Unexpected error " + e.getMessage());
        } finally {
            sendingQueue.terminate();
            Services.REGISTRY.deleteClient(this);
            socket.close();
        }
    }

    /**
     * Send message to client computer.
     */
    public void sendMessageToClientComputer(String message) {
        try {
            socket.write(message);
        } catch (IOException e) {
            // XXX [Oleg] Awful! (Tell me please later, how you've solved this, as I haven't decided
            // how to handle this. Maybe we should just log this?
            // XXX [Yan] seems there is nothing we can do
            Services.ADMIN_LOGGER.write("Can't send message because of " + e.getMessage());
        }
    }

    /**
     * Process unhandled message
     *
     * @param message Raw text received from socket
     */
    private void handleMessage(String message) throws IOException {
        if (message.startsWith("$change")) {
            messageHandler.changeRoom(parseProtocol(message));
        } else if (message.startsWith("$pm")) {
            messageHandler.sendPersonalMessage(parseProtocol(message));
        } else if (message.startsWith("!")) {
            messageHandler.markMessageInDb(message.split("!", 2)[1]);
        } else if (message.startsWith("$")) {
            messageHandler.sendMessageToRoom(parseProtocol(message));
        }
    }


    // ------------------------------------------ AUTHENTIFICATION METHODS

    private class LoginData {
        public String name;
        public String password;

        @Override
        public String toString() {
            return String.format("name: '%s' password: '%s'", name, password);
        }
    }

    private LoginData askLogin() throws IOException {
        LoginData loginData = new LoginData();

        socket.write("Input login");
        loginData.name = parseProtocol(socket.read());

        socket.write("Input password");
        loginData.password = parseProtocol(socket.read());

        return loginData;
    }

    /**
     * Perform beginning negotiations with the client.
     * Login them, determine the room and register in ClientRegistry.
     *
     * @return <code>true</code> if client has successfully registered and <code>false</code> otherwise.
     */
    private boolean registerClient() throws IOException {
        // Getting login data and checking if they match the data in DB.
        LoginData loginData = askLogin();
        while (!(Services.dbService.clientExists(loginData.name) &&
                Services.dbService.getPasswordByClientName(loginData.name).equals(loginData.password))) {
            socket.write("Wrong login-password combination.");
            Services.ADMIN_LOGGER.write("Client entered wrong login-password combination: \n" + loginData);
            loginData = askLogin();

        }
        this.name = loginData.name;
        Services.ADMIN_LOGGER.write(String.format("Client '%s' has entered valid auth info.", this.name));

        // Trying to register client in the Registry
        String room = Services.dbService.getRoomNameByClientName(getName());
        boolean result = Services.REGISTRY.addClient(this, room);
        if (!result) {
            Services.ADMIN_LOGGER.write(String.format("Client '%s' has already entered from another computer.", this.name));
            socket.write("You are already logged in from another computer. Disconnect from it first.\n" +
                    "Bye.");
            return false;
        } else {
            Services.ADMIN_LOGGER.write(String.format("Client '%s' has successfully entered.", this.name));
            socket.write(String.format("You have logged in. Your room is %s.\n" +
                    "\"Possible rooms are: %s.", Services.REGISTRY.getClientRoom(getName()),
                    Services.REGISTRY.getRoomNames().toString()));

            return true;
        }
    }

    private void writeWelcomeMessage() throws IOException {
        socket.write("To change room type 'change new_room_name'\n" +
                "To write personal message type 'pm recipient message_text'\n" +
                "To exit type 'exit'\n" +
                "All other messages will be send to the whole room.");
    }

    public boolean add(Message message) {
        return sendingQueue.add(message);

    }

    public boolean addAllFront(List<Message> messages) {
        return sendingQueue.addAllFront(messages);

    }

    /**
     * deletes dollar from the beginning of message
     * @param message $text
     * @return text
     */
    private String parseProtocol(String message) {
        return message.split("\\$", 2)[1];
    }

}
